package math.vectorSpace.R3;

import java.awt.Graphics2D;
import math.mathExtended.DoubleComparator;
import math.mathExtended.MathExtended;
import math.vectorSpace.R3.visualization.Visualizable;
import math.vectorSpace.R3.visualization.Render3D;
import math.vectorSpace.R3.visualization.VisualizationProperties;

/**
 * Line3D class for creating and performing related operations on lines in 3D space
 * Line objects store a reference point that is on the line and a direction vector
 * 
 * @author Shimu Wu
 * @version 1.0
 */
public class Line3D implements Visualizable {

    private Point3D refPoint = new Point3D();

    private Vector3D dirVector;

    private static final DoubleComparator comparator = new DoubleComparator();
    
    /**
     *  Creates a line through the origin
     */
    public Line3D(Vector3D dirVector) {
        this.dirVector = dirVector;
    }

    /**
     *  Creates a line through the given point
     */
    public Line3D(Point3D refPoint, Vector3D dirVector) {
        this.refPoint = refPoint;
        this.dirVector = dirVector;
    }

    /**
     *  Creates a line through the given point and the origin
     */
    public Line3D(Point3D point) {
        this.dirVector = new Vector3D(point);
    }

    /**
     *  Creates a line through two points
     */
    public Line3D(Point3D point1, Point3D point2) {
        this.refPoint = point1;
        this.dirVector = new Vector3D(point1, point2);
    }

    /**
     *  Creates a line through the origin with the specified direction vector
     */
    public Line3D(double vX, double vY, double vZ) {
        this.dirVector = new Vector3D(vX, vY, vZ);
    }

    /**
     *  Creates a line with the specified reference point coordinates
     *  and direction vector
     */
    public Line3D(double rX, double rY, double rZ, double vX, double vY,
            double vZ) {
        this.refPoint = new Point3D(rX, rY, rZ);
        this.dirVector = new Vector3D(vX, vY, vZ);
    }

    /**
     *  Returns the reference point of this line
     */
    public Point3D getRefPoint() {
        return refPoint;
    }

    /**
     *  Returns the direction vector of this line
     */
    public Vector3D getDirVector() {
        return dirVector;
    }

    /**
     *  Sets the reference point of the line to a new location
     */
    public void setRefPoint(Point3D point) {
        this.refPoint = point;
    }

    /**
     *  Sets the direction vector of the line to another vector
     */
    public void setDirVector(Vector3D vector) {
        this.dirVector = vector;
    }

    /**
     *  Returns the parameter when the line is at the given x value.
     *  
     *  @return the parameter when the line is at the given x value.
     *  @return Double.NaN if every point on this line has this same x value.
     *  i.e. any parameter will work for this x value (the line has a x-component 
     *  of zero)
     *  @return Double.Infinity if the given x value does not lie on the line.
     */
    public double parameterAtX(double x) {
        return (x - refPoint.getX()) / (dirVector.getX());
    }

    /**
     *  Returns the parameter when the line is at the given y value.
     *  
     *  @return the parameter when the line is at the given y value.
     *  @return Double.NaN if every point on this line has this same y value.
     *  i.e. any parameter will work for this y value (the line has a y-component 
     *  of zero)
     *  @return Double.Infinity if the given y value does not lie on the line.
     */
    public double parameterAtY(double y) {
        return (y - refPoint.getY()) / dirVector.getY();
    }

    /**
     *  Returns the parameter when the line is at the given z value.
     *  
     *  @return the parameter when the line is at the given z value.
     *  @return Double.NaN if every point on this line has this same z value.
     *  i.e. any parameter will work for this z value (the line has a z-component 
     *  of zero)
     *  @return Double.Infinity if the given z value does not lie on the line.
     */
    public double parameterAtZ(double z) {
        return (z - refPoint.getZ()) / (dirVector.getZ());
    }

    public double parameterAt(Point3D p) {
        double x = parameterAtX(p.getX());
        double y = parameterAtY(p.getY());
        double z = parameterAtZ(p.getZ());

        if (((x == y) || (Double.isNaN(x)) || (Double.isNaN(y)))
                && ((y == z) || (Double.isNaN(y)) || (Double.isNaN(z)))
                && ((x == z) || (Double.isNaN(x)) || Double.isNaN(z))) {
            return x;
        } else {
            return Double.NaN;
        }
    }

    /**
     *  Returns the coordinate of the point when the line is at the given x value.
     *  
     *  @see #evaluateAt(double)
     */
    public Point3D pointAtX(double x) {
        return evaluateAt(parameterAtX(x));
    }

    /**
     *  Returns the coordinate of the point when the line is at the given y value.
     *  
     *  @see #evaluateAt(double)
     */
    public Point3D pointAtY(double y) {
        return evaluateAt(parameterAtY(y));
    }

    /**
     *  Returns the coordinate of the point when the line is at the given z value.
     *  
     *  @see #evaluateAt(double)
     */
    public Point3D pointAtZ(double z) {
        return evaluateAt(parameterAtZ(z));
    }

    /**
     *  Returns the point at the given parameter.
     *  
     * @param t
     * @return  
     */
    public Point3D evaluateAt(double t) {
        /*
        if (Double.isInfinite(t)) {
        // Returns an invalid point
        return new Point3D(Double.NaN, Double.NaN, Double.NaN);
        } else if (Double.isNaN(t)){
        // This means the parameter can be anything
        // Hence return an arbitrary point on the line
        return refPoint;
        }
         */
        return refPoint.add(dirVector.scale(t));
    }

    /**
     * Returns true if the given line is the same as this line.
     * Two lines are considered to be equal if their direction
     * vectors are colinear and the reference point of one line
     * satisfy the equation for the other line. 
     * 
     * @param line 
     * @return true if this line and the given line are equal, 
     *         false otherwise
     */
    public boolean equal(Line3D line) {
        if (dirVector.isColinear(line.dirVector) && line.refPoint.on(this)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns true if this line is the same as the given line, false otherwise.
     * 
     * @param line a Line3D object
     * @return true if this line is the same as the given line, false otherwise
     * @see equal
     */
    public boolean on(Line3D line) {
        return this.equal(line);
    }

    /**
     * Returns true if this line lies on the given plane, false otherwise
     * 
     * @param plane a {@link #Plane3D} object
     * @return true if this line lies on the given plane, false otherwise
     */
    public boolean on(Plane3D plane) {
        if ((plane.getNormal().dot(dirVector) == 0) && refPoint.on(plane)) {
            return true;
        }
        return false;
    }

    /**
     * Returns true if the given point lies on this line, false otherwise.
     * @param point a Point3D object
     * @return true if the given point lies on this line, false otherwise
     * @see #Point3D.on(Line3D line)
     */
    public boolean contains(Point3D point) {
        return point.on(this);
    }

    /**
     *  Finds the distance between this line and a given point
     *  
     *  @param point 
     *  @return the distance between this line and a given point
     *  @see #Point3D.distance(Line3D line)
     */
    public double distance(Point3D point) {
        return point.distance(this);
    }

    /**
     * Finds the distance between this line and a given line.
     * Can be used to find the distance between skew lines.
     * 
     * @param line a Line3D object
     * @return the distance between this line and the given line
     */
    public double distance(Line3D line) {
        if (dirVector.isColinear(line.dirVector)) {
            return refPoint.distance(line);
        }

        // Finds a vector that is perpendicular to both lines
        Vector3D vectorPerp = dirVector.cross(line.dirVector);
        // Create a vector that connects two random points, one on each line 
        Vector3D lineVector = new Vector3D(refPoint, line.refPoint);

        // The magnitude of the projection of the second vector 
        // onto the first will give the distance
        return lineVector.projectMag(vectorPerp);
    }

    /**
     * Returns the distance between this line and a given plane.
     * 
     * @param plane a {@link Plane3D} object
     * @return the distance between this line and the given plane
     */
    public double distance(Plane3D plane) {
        if (!plane.getNormal().isOrthogonal(dirVector)) {
            return 0;
        } else {
            Vector3D tempVector = new Vector3D(refPoint, plane.getRefPoint());
            return tempVector.projectMag(plane.getNormal());
        }
    }

    /**
     *  Projects a given point onto this line.
     *  
     *  @param point 
     *  @return a Point3D representing the projection of this 
     *          point onto the given line
     *  @see #Point3D.project(Line3D line)
     */
    public Point3D project(Point3D point) {
        return point.project(this);
    }

    /**
     * Projects this line onto the surface of the given plane
     * 
     * @param plane a {@link Plane3D} object
     * @return a Line3D object representing the projection of this
     *         line onto the given plane
     */
    public Line3D project(Plane3D plane) {
        Point3D point1 = refPoint.project(plane);
        Point3D point2 = refPoint.add(dirVector).project(plane);
        return new Line3D(point1, point2);
    }

    /**
     * Finds the intersection between this line and the given line 
     * 
     * @param line a Line3D object
     * @return a {@link Point3D} object representing the intersection between
     *         this line and a given line
     */
    public Point3D intersect(Line3D line) {
        double t, s; // The parameters
        double x, y, z, vX, vY, vZ; // The variables for the parametric equation of a line
        double x2, y2, z2, vX2, vY2, vZ2; // The variables for the second line
        double scaleFactor;

        x = refPoint.getX();
        y = refPoint.getY();
        z = refPoint.getZ();
        vX = dirVector.getX();
        vY = dirVector.getY();
        vZ = dirVector.getZ();
        x2 = line.refPoint.getX();
        y2 = line.refPoint.getY();
        z2 = line.refPoint.getZ();
        vX2 = line.dirVector.getX();
        vY2 = line.dirVector.getY();
        vZ2 = line.dirVector.getZ();

        scaleFactor = vX / vY;

        s = (x - x2 + scaleFactor * (y2 - y)) / (vX2 - scaleFactor * vY2);
        t = (x2 - x + vX2 * s) / vX;

        if (comparator.equal(s, t)
                && comparator.equal((z + vZ * t), (z2 + vZ2 * s))) {
            // If this is true, then the solution found is valid
            return (new Point3D(this.evaluateAt(t)));
        } else {
            // If the parameters found do not satisfy this third 
            // equation then there is no solution
            return null;
        }
    }

    /**
     * Returns the intersection between this line and a given plane.
     * 
     * @param plane a {@link Plane3D} object
     * @return a {@link Point3D} representing the intersection between this 
     *         line and a given plane
     */
    public Point3D intersect(Plane3D plane) {
        // Check to see if the line is parallel to the plane
        if (plane.getNormal().isOrthogonal(dirVector)) {
            // Check to see if the line lies on the plane
            if (refPoint.on(plane)) {
                // If the line lies on the plane, infinite solutions 
                return null;
            }
            // Otherwise, no solution
            return null;
        } else {
            // If the line is not parallel, then there is a point of intersection
            double t = -((plane.getA() * refPoint.getX())
                    + (plane.getB() * refPoint.getY())
                    + (plane.getC() * refPoint.getZ()) + plane.getD())
                    / ((plane.getA() * dirVector.getX())
                    + (plane.getB() * dirVector.getY()) + (plane.getC() * dirVector.getZ()));
            return (evaluateAt(t));
        }
    }

    /**
     * Returns the String representation of this line, which takes the form:
     * 
     * Line3D [x, y, z] = (x0, y0, z0) + t[x1, y1, z1]
     * 
     * where (x0, y0, z0) is the coordinates of the reference point
     * and [x1, y1, z1] is the components of the direction vector
     */
    @Override
    public String toString() {
        return ("Line3D [x,y,z] = (" + refPoint.getX() + ", " + refPoint.getY()
                + ", " + refPoint.getZ() + ") + t[" + dirVector.getX() + ", "
                + dirVector.getY() + ", " + dirVector.getZ() + "]");
    }

    /**
     * Draws this Line3D by converting it into a finite length segment. The
     * finite length segment is converted by adding and subtracting this 
     * Line3D's direction vector scaled by a factor of 0.5.
     * 
     * @param render
     * @param graphics
     * @param properties 
     * @param length  
     */
    public void draw(Render3D render,
            Graphics2D graphics, VisualizationProperties properties, 
            double length) {
        Vector3D delta = this.dirVector.scale(length / 2.0);

        // Converts this infinite line into a finite segment so it can be drawn
        Segment3D segment = new Segment3D(refPoint.subtract(delta),
                refPoint.add(delta));
        
        segment.draw(render, graphics, properties);
    }

    @Override
    public void draw(Render3D render, Graphics2D graphics,
            VisualizationProperties properties) {
        draw(render, graphics, properties, 1000.0);
    }
}
